很多時候會希望一個類別只會有唯一一個實體,像是 DB 的接口、應用程式的偏好設定、 一個中國。
這時就會需要 Singleton Pattern 了。它的實作很簡單,也很方便。但是要小心不要過度使用了,Singleton 也可以是一個 anti pattern,這部分會在下半段說明。
想要確保一個類別只有一個實體的話,要怎麼做到呢?首先標準的物件導向語言都有提供 private
, protected
等關鍵字來限制存取範圍(通常稱為 scope),只要使用以上關鍵字來限制建構子的存取範圍,再搭配公開的靜態方法 (public static method)、靜態成員變數(static field),就能讓外部存取指定的實例,而且確保該實例是全域唯一的實體。
上面這段話很抽象嗎?直接來看程式碼吧!
以 Java 為例:
class DBHelper {
//通常我們會命名該實體為 'instance'
private static DBHelper instance;
//只能透過這個 method 來拿到唯一實體
public static DBHelper getInstance() {
if (instance == null) {
instance = new DBHelper();
}
return instance;
}
//建構子不能公開
private DBHelper() {}
//DBHelper 提供的 public method,
public boolean insert(SomeBean bean) {
...
}
}
這邊還包含了一個 null 檢查,為什麼呢?因為有時候不希望一開始就佔了系統資源,像是 Android App 的開啟時間是很珍貴的,如果開啟時間太久使用者可是會刪掉 App 的!而這種需要才建立實例的方式就叫做 lazy initialization。
我知道這時候有人就會開始說,你這樣做不對喔!沒有考慮到 thread safe,應該用 double checked-locking 才行。的確很多網路上的資源都有提到這個,多執行緒會是一個問題。但是想想喔,在怎樣的情況下,會有兩個不一樣的 thread 會呼叫 getInstance(),而且還是在還沒初始化的情況下。
我相信這種情況應該很少遇到才對(尤其是 App 開發),以我本身的經驗,大部分的人在應用程式初始化的時候就建立好實例了。在這種情況下,說不定連 lazy initialization 都不用。所以在這裡想要提供大家另一個觀點,在使用 double checked-locking 之前,要多想想為什麼需要他,以現有的架構下有可能從不同的 thread 去呼叫 getInstance()
嗎?只在指定的 thread 上呼叫是可行的嗎?還是其實跟本不需要 Singleton?
不知道有沒有讀者跟我一樣不喜歡 double checked-locking ,看看 wiki 連結的內容,要考慮的問題遠比你想像中的多!
前面有提到 Singleton 有可能過度使用,以下提供幾點:
class AccountService {
private AccountRepo accountRepo =
//不要這樣做,方便不代表隨便
DBHelper.getInstance().getAccountRepo()
public AccountService() {}
public boolean hasLogin() {
//還有可能忘記上面就拿過 AccountRepo 了
Account account = DBHelper.getInstance()
.getAccountRepo()
.getAccount()
...
}
...
}
class AccountService {
private AccountRepo accountRepo;
//真正需要的只有 AccountRepo,不用認識 DBHelper,在閱讀程式碼的時候也會比較清楚他們的相依性
public AccountService(AccountRepo accountRepo) {
this.accountRepo = accountRepo;
}
...
}
class AccountServiceTest {
@Test
public void verifyAccount() {
//測試好寫多了,要怎樣的測試資料都很好做
AccountRepo repo = new FakeAccountRepo("John", true);
AccountService service = new AccountService(repo);
...
assertTrue(service.hasLogin())
}
}
Singleton 要解決的問題是唯一實例
。同一個系統裡面只會有同一種狀態,但是由於他方便的特性(可以隨時呼叫靜態方法),會讓大家開始濫用。因此在使用時要隨時注意有沒有犯了文章上面的錯誤。
另一方面,很多語言都有提供 DI 以及 IOC 框架,這些框架可以幫你建立實例,只要預先寫好物件的相依關係即可。同時還可以幫你解決 Singleton 要做的事情,可以不用自己實作 。但是要注意一但用了這些框架,Singleton 所提供的保護將不會存在,如果有一個新人不知道框架的運作方式的話,還是有可能會有問題的。
沒有最好的解法,只有最適合的解法,要使用 Singleton 還是 DI 框架,還是要各個團隊自己來決定,今天的文章就到這邊,感謝大家的閱讀。
作者:Yanbin